In my last blog post I talked about how to use PowerShell to instantiate an MSAL Confidential Client Application to acquire an access token using Client Credentials Grant flow. In this post we will use PowerShell to instantiate an MSAL Public Client Application to perform an Authorization Code Grant flow to obtain a delegated permission Access Token for Microsoft Graph. We will then use that access token to call Microsoft Graph to configure a signing certificate for our SAML Application Service Principal. Just a quick refresher that a certificate is always required when setting up SAML Sigle Sign-On feature for an Enterprise App in Azure AD.
Pre-requisites
To run the script in this blog you should have the following:
1) A SAML-based SSO Enterprise Application you want to configure a signing certificate for. Get the Object ID of this Application in the Properties blade of the Enterprise App – we will need it for the script. The script in this blog performs the same thing as doing the following UI action in the portal:
Azure Active Directory -> Enterprise Application -> Pick the correct App -> Single sign-on -> click ‘Edit’ link in the SAML Signing Certificate section -> Import Certificate
If you are trying to automate a SAML-based SSO Application, take a look at the documentation Automate SAML-based SSO app configuration with Microsoft Graph API. This blog can help with step 4, Configure Signing Certifcate, of that article.
2) An app registration to sign in a user and get an access token for Microsoft Graph. Get the Application (client) ID of this app in the Overview section – we will need it for the script. This application should have the following App Registration configuration:
Supported account types | Accounts in this organizational directory only |
Redirect URIs | http://localhost under ‘Mobile and desktop applications’ platform |
API permissions | Microsoft Graph – Delegated permissions: Application.ReadWrite.All and User.Read (Make sure you grant Admin consent to these permissions) |
3) The user who logs in to get the MS Graph Access Token should be one of the following Azure AD Administrative Role – this is required in order to make a change to the Service Principal:
- Cloud Application Administrator
- Application Administrator
- Global Administrator
Signing Certificate
We will need to have a certificate to configure for our application. You can either create a self-signed certificate (using either PowerShell or OpenSSL as shown below) or obtain one from your Trusted Certificate Authority. We will need the following certificate components for our script:
- public key (typically a .cer file)
- private key in PKCS#12 format (in .pfx file)
- password for the private key (pfx file)
Note: It is important to have the private key in PKCS#12 format since Azure AD does not support other format types. Using the wrong format can result in the the error “Invalid certificate: Key value is invalid certificate” when using MS Graph to PATCH the Service Principal with a keyCredentials containing the certificate info
Using PowerShell to create a self-signed certificate
The following PowerShell script can be used to create a self-signed certificate and then export both the private key and public key out to a .pfx and and a .cer files
# fqdn - this is used for the 'issued to' and 'issued by' field of the certificate # pwd - password for exporting the certificate private key # location - path to folder where both the pfx and cer file will be written to, for example C:\users\john\Documents Param( [Parameter(Mandatory=$true)] [string]$fqdn, [Parameter(Mandatory=$true)] [string]$pwd, [Parameter(Mandatory=$true)] [string]$location ) if (!$PSBoundParameters.ContainsKey('location')) { $location = "." } $cert = New-SelfSignedCertificate -certstorelocation cert:\currentuser\my -DnsName $fqdn $pwdSecure = ConvertTo-SecureString -String $pwd -Force -AsPlainText $path = 'cert:\currentuser\my\' + $cert.Thumbprint $cerFile = $location + "\\" + $fqdn + ".cer" $pfxFile = $location + "\\" + $fqdn + ".pfx" Export-PfxCertificate -cert $path -FilePath $pfxFile -Password $pwdSecure Export-Certificate -cert $path -FilePath $cerFile
Using OpenSSL to create a self-signed certificate
If you don’t have OpenSSL installed already, refer to the OpenSSL documentation for building and installation instruction. For Windows users, this StackOverflow discussion has some useful information on how to download OpenSSL for Windows.
1) Run the following openssl command to create a public key file (crt) and private key file (pem) with your info. See https://www.digicert.com/kb/ssl-support/openssl-quick-reference-guide.htm for some openssl reference guide. openssl req-x509 -sha256 -days 365 -newkey rsa:2048 -keyout "C:\Users\path\privateKey.key" -out "C:\Users\path\certificate.crt" -subj '/C=your country/ST=your state/L=your locality/O=Your Company, Inc./OU=your Organizational Unit/CN=yourdomain.com' 2) convert the pem file to pfx file with your info: openssl pkcs12 -export -out "C:\Users\path\certificate.pfx" -inkey "C:\Users\path\privateKey.key" -in "C:\Users\path\certificate.crt" -passout pass:your password -passin pass:your password
3) convert the crt file to DER encoded binary X.509 .cer file:
On Windows, double-click on the crt file to launch the certificate wizard. Go to ‘Details’ tab and click on ‘Copy to File…’ button:
Click ‘Next’ on the ‘Welcome to the Certificate Export Wizard’ page. Choose ‘DER encoded binary X.509 (cer)’ option and save to cer file.
Configuring the signing certificate using the MS Graph request
At this point we should have a pfx file, a cer file, and a pfx file password. We will use all of this information to construct the following MS Graph request:
PATCH https://graph.microsoft.com/v1.0/serviceprincipals/<Service Principal Object ID>
Request body:
Note: This is not a complete request body. For brevity, I am only emphasizing the main part related to private and public key.
{ "keyCredentials":[ { ... "type": "AsymmetricX509Cert", "usage": "Sign", "key": Base64-encoded private key }, { ... "type": "AsymmetricX509Cert", "usage": "Verify", "key": Base64-encoded public key } ], "passwordCredentials": [ { ... "secretText": "password for the pfx file" } ] }
Note: the passwordCredentials is required in the above request. Omitting it may result in the error “The value for the property \”usage\” in one of your credentials is invalid. Acceptable values are Sign, Verify.”
There is also a PowerShell script to generate the above json body from the certificate files located here.
Use the following PowerShell script with your info to send a PATCH request to configure the signing certificate for this SAML Application. The script will attempt to download MSAL.Net module if it does not already exist on the machine and then use MSAL.Net to sign in to Azure AD. You may need to run the following commands first to update both Nuget package and PowerShellGet.
## Update Nuget Package and PowerShellGet Module Install-PackageProvider NuGet -Force Install-Module PowerShellGet -Force
Note: this script will replace the current keyCredentials and passwordCredentials configured for this Service Principal. If you need to do an append operation, you will need to get the current selection first and append it to the payload.
$ClientID = "<client ID>" $loginURL = "https://login.microsoftonline.com" $tenantdomain = "<tenant>.onmicrosoft.com" $redirectURL = "http://localhost" # this reply URL is needed for PowerShell Core [string[]] $Scopes = "https://graph.microsoft.com/.default" $pfxpath = "C:\Users\path\certificate.pfx" # path to pfx file $cerpath = "C:\Users\path\certificate.cer" # path to cer file $SPOID = "<Service Principal Object ID>" $graphuri = "https://graph.microsoft.com/v1.0/serviceprincipals/$SPOID" $password = "<password for pfx file>" # password for the pfx file $CertDisplayName = "CN = <Your Cert Display Name>" # "CN=contoso" # choose the correct folder name for MSAL based on PowerShell version 5.1 (.Net) or PowerShell Core (.Net Core) # script is tested on both PS 5.1 and PS Core 7 if ($PSVersionTable.PSVersion.Major -gt 5) { $core = $true $foldername = "netcoreapp2.1" } else { $core = $false $foldername = "net45" } # Download MSAL.Net module to a local folder if it does not exist there if ( ! (Get-ChildItem $HOME/MSAL/lib/Microsoft.Identity.Client.* -erroraction ignore) ) { install-package -Source nuget.org -ProviderName nuget -SkipDependencies Microsoft.Identity.Client -Destination $HOME/MSAL/lib -force -forcebootstrap | out-null } # Load the MSAL assembly -- needed once per PowerShell session [System.Reflection.Assembly]::LoadFrom((Get-ChildItem $HOME/MSAL/lib/Microsoft.Identity.Client.*/lib/$foldername/Microsoft.Identity.Client.dll).fullname) | out-null $global:app = $null $ClientApplicationBuilder = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientID) [void]$ClientApplicationBuilder.WithAuthority($("$loginURL/$tenantdomain")) [void]$ClientApplicationBuilder.WithRedirectUri($redirectURL) $global:app = $ClientApplicationBuilder.Build() Function Get-GraphAccessTokenFromMSAL { [Microsoft.Identity.Client.AuthenticationResult] $authResult = $null $AquireTokenParameters = $global:app.AcquireTokenInteractive($Scopes) [IntPtr] $ParentWindow = [System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle if ($ParentWindow) { [void]$AquireTokenParameters.WithParentActivityOrWindow($ParentWindow) } try { $authResult = $AquireTokenParameters.ExecuteAsync().GetAwaiter().GetResult() } catch { $ErrorMessage = $_.Exception.Message Write-Host $ErrorMessage } return $authResult } $myvar = Get-GraphAccessTokenFromMSAL if ($myvar) { $GraphAccessToken = $myvar.AccessToken Write-Host "Access Token: " $myvar.AccessToken # this is for PowerShell Core $Secure_String_Pwd = ConvertTo-SecureString $password -AsPlainText -Force # reading certificate files and creating Certificate Object if ($core) { $pfx_cert = get-content $pfxpath -AsByteStream -Raw $cer_cert = get-content $cerpath -AsByteStream -Raw $cert = Get-PfxCertificate -FilePath $pfxpath -Password $Secure_String_Pwd } else { $pfx_cert = get-content $pfxpath -Encoding Byte $cer_cert = get-content $cerpath -Encoding Byte # Write-Host "Enter password for the pfx file..." # calling Get-PfxCertificate in PowerShell 5.1 prompts for password # $cert = Get-PfxCertificate -FilePath $pfxpath $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($pfxpath, $password) } # base 64 encode the private key and public key $base64pfx = [System.Convert]::ToBase64String($pfx_cert) $base64cer = [System.Convert]::ToBase64String($cer_cert) # getting id for the keyCredential object $guid1 = New-Guid $guid1 = $guid1.ToString() $guid2 = New-Guid $guid2 = $guid2.ToString() # get the custom key identifier from the certificate thumbprint: $hasher = [System.Security.Cryptography.HashAlgorithm]::Create('sha256') $hash = $hasher.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($cert.Thumbprint)) $customKeyIdentifier = [System.Convert]::ToBase64String($hash) # get end date and start date for our keycredentials $endDateTime = ($cert.NotAfter).ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" ) $startDateTime = ($cert.NotBefore).ToUniversalTime().ToString( "yyyy-MM-ddTHH:mm:ssZ" ) # building our json payload $object = [ordered]@{ keyCredentials = @( [ordered]@{ customKeyIdentifier = $customKeyIdentifier endDateTime = $endDateTime keyId = $guid1 startDateTime = $startDateTime type = "AsymmetricX509Cert" usage = "Sign" key = $base64pfx displayName = $CertDisplayName }, [ordered]@{ customKeyIdentifier = $customKeyIdentifier endDateTime = $endDateTime keyId = $guid2 startDateTime = $startDateTime type = "AsymmetricX509Cert" usage = "Verify" key = $base64cer displayName = $CertDisplayName } ) passwordCredentials = @( [ordered]@{ customKeyIdentifier = $customKeyIdentifier keyId = $guid1 endDateTime = $endDateTime startDateTime = $startDateTime secretText = $password } ) } $json = $object | ConvertTo-Json -Depth 99 Write-Host "JSON Payload:" Write-Output $json # Request Header $Header = @{} $Header.Add("Authorization","Bearer $($GraphAccessToken)") $Header.Add("Content-Type","application/json") try { Invoke-RestMethod -Uri $graphuri -Method "PATCH" -Headers $Header -Body $json } catch { # Dig into the exception to get the Response details. # Note that value__ is not a typo. Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__ Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription } Write-Host "Complete Request" } else { Write-Host "Fail to get Access Token" }
If the script completes successfully we should see the thumbprint of our certificate showing up for this application:
Last Updated on 8/19/2022
Thanks for sharing this, it’s a very useful ice of code. I just posted a link to it on https://feedback.azure.com/forums/169401-azure-active-directory/suggestions/37970713-powershell-saml-app-automation
Is there a way to programmatically create a new Microsoft Azure Federated SSO Certificate (akin to hitting the New Certificate button)? Our SAML SPs are not going to trust a self signed cert and I don’t want to purchase a cert unless I need to.
I think the Append step is important too as generally you want an orderly cutover on the SAML SP side to minimize impact.
I think it might be possible to do this without Global Admin. ‘Cloud Application Administrator’ and ‘Application Administrator’ might be enough. I’ll try it.
I believe that New Certificate button is still creating a self signed certificate. It’s not issued by any trusted CA. The value ‘Microsoft Azure Federated SSO’ in the issued to and issued by field you can probably set that yourself as well when using PowerShell to create a new self signed cert. On the Admin role, you are correct as that’s noted in Pre-requisite #3 in the blog.
Hi! thanks for sharing this.. but I have another question which is the other way round. How can I get the existing certificate details of SAML application using powershell?
You can use PowerShell to issue an MS Graph call to the same Service Principal object per https://docs.microsoft.com/en-us/graph/api/serviceprincipal-get?view=graph-rest-1.0&tabs=http. The certificate info is in the keycredentials attribute. You should be able to read back the values you uploaded except for the ‘value’ properties which will always be returned as null and that’s intended.
Hi!
I was able to retrieve ‘some’ certificate details from the keycredentials of the service principal, but it looks like the ‘CustomKeyIdentifier’ contains the hash-value after converting it to base64 string.
However the portal displays the thumbprint of the certificate. How to get this using the Graph API?
In contrast the keycredentials attribute of an application contains an customKeyIdentifier which could be translated to the thumbprint of the used certificate successfully.
Edit: It seems ‘hash’ and ‘thumbprint’ references the same thing but after converting my “thumbprint” reads like ‘hRUjwpH4….=’ but should be ‘3910C45990F…‘
What am I missing here?
There is not any direct way to get that thumbprint from MS Graph. If you use the above PS script to upload the certificate then you should be able to query back the Service Principal’s keyCredentials’s customKeyIdentifier and reverse the process to get back the certificate thumbprint.
Know this is an old thread but….
You cannot reverse the SHA256 hash by definition that it’s a one-way function. Thus, it is next to impossible to get the thumbprint from having just the hash value written to CustomKeyIdentifier.
It’s frustrating that in the Azure AD portal, the thumbprint is shown as a way to identify the certificate and not the CustomKeyIdentifier (hash of thumbprint). When given the thumbprint of a cert and the hash of another, the only way to confirm whether the two certs are identical is to compute the hash of the former for comparison to the later.
Thanks for providing feedback. It would be great if you can provide this feedback at the following link:
https://feedback.azure.com/d365community/forum/22920db1-ad25-ec11-b6e6-000d3a4f0789
I don’t know when this was added to the graph api but it is now very easy to do this… Just replace ${SP.ObjectID} below with the object ID and you’re done in a single command
az rest –method POST –uri ‘https://graph.microsoft.com/v1.0/servicePrincipals/${SP.ObjectID}/addTokenSigningCertificate’
Thanks for sharing. Indeed this looks like to be a recently added functionality.